| 季度 | 实际销售 | 上年同季预测 | 绝对百分比误差 |
|---|---|---|---|
| 2024Q1 | 110 | 100 | \(|110-100|/110=9.1\%\) |
| 2024Q2 | 96 | 90 | \(|96-90|/96=6.3\%\) |
先让模型回答“能否持续优于这条朴素规则”。MAPE 是各验证期绝对百分比误差的平均值;实际值为 0 或接近 0 时应换指标或单独披露。
2015–2022 训练 → 2023–2024 验证 → 2025 再次检查
| 学生先掌握 | 完整代码中的变量 | 检查问题 |
|---|---|---|
| 上年同季基线 | seasonal_baseline |
是否使用相同验证窗? |
| 时间边界 | train / validation |
验证期是否进入拟合? |
| 误差 | baseline_mape / regression_mape |
候选是否真正优于基线? |
| 拒绝规则 | decision_status |
何时保持基线或暂停? |
理解这四项后,再学习怎样选择财报披露版本、把累计值换算为单季值、估计预测区间,以及发现数据问题时怎样检查原因。
| 要素 | 课堂口径 |
|---|---|
| 本地资产 | stock/financial_statement.h5(key: financial_data) |
| 对象与期间 | 上海百联股份 600827.SH,2015–2025 年季度营业收入 |
| 时间切分 | 2015–2022 训练,2023–2024 验证,2025 滚动再次检查 |
| 决策 | 预算与排班;验证期 MAPE ≤ 15% 才允许作为基线 |
Walmart 数据只用于完成平台练习。中国主案例先核实财报字段是累计值还是单季值,再与“上年同季”季节朴素基线比较,并报告披露版本、结构变化与预测区间。
| 风险 | 本章处理 | 检查依据 |
|---|---|---|
| 累计值误当单季值 | Q1 取累计值,Q2–Q4 减去同年上期累计值 | 单季值与季度序号 |
| 同一报告期多版本 | 有 ann_date 时保留最晚披露;无版本字段且重复时停止实证 |
去重规则与重复计数 |
| 风险 | 本章处理 | 检查依据 |
|---|---|---|
| 季节性被趋势掩盖 | 朴素基线使用上年同季,回归加入季度虚拟变量 | 同一验证窗口的 MAPE |
| 点预测被误当保证 | 用训练残差构造课堂近似 95% 区间 | 上下界及覆盖说明 |
from pathlib import Path # 导入Path以选择跨平台数据根目录
import pandas as pd # 导入Pandas用于财报时间序列处理
import numpy as np # 导入NumPy用于有限值检查要求
from sklearn.linear_model import LinearRegression # 导入可解释线性回归作为候选模型
from sklearn.metrics import r2_score # 导入R方用于描述验证期拟合边界
linux_root = Path('/home/ubuntu/r2_data_mount/data') # 指定Linux课程数据根目录
windows_root = Path(r'C:\qiufei\data') # 指定Windows课程数据根目录
data_root = linux_root if linux_root.exists() else windows_root # 选择可用数据根目录
financial_path = data_root / 'stock' / 'financial_statement.h5' # 定位财务报表本地快照
is_real_data_available = financial_path.exists() # 将数据状态与渲染成功明确分离
if not is_real_data_available: # 数据缺失时立即报告暂停但保持课件可渲染
print(f'未找到课程数据文件:{financial_path};请从课程数据下载入口获取后重新运行。') # 提示读者下载所需文件if is_real_data_available: # 只在真实本地快照存在时定义披露选择约定
financials = pd.read_hdf(financial_path, key='financial_data', where="order_book_id == '600827.XSHG'", columns=['order_book_id','quarter','info_date','operating_revenue']).rename(columns={'order_book_id':'ts_code','quarter':'end_date','info_date':'ann_date','operating_revenue':'total_revenue'}) # 只读取目标公司的必需财报字段并标准化
financials['ts_code'] = financials['ts_code'].str.replace('.XSHG','.SH',regex=False).str.replace('.XSHE','.SZ',regex=False) # 统一证券代码后缀
financials['end_date'] = pd.PeriodIndex(financials['end_date'].str.upper(), freq='Q').end_time.normalize() # 将季度键转换为报告期末日期
def select_latest_disclosure(frame,value_fields): # 统一财报披露版本选择约定
required={'ts_code','end_date',*value_fields}; versions=[c for c in ['ann_date','f_ann_date'] if c in frame.columns] # 固定键与可接受版本字段
if not required<=set(frame.columns): raise ValueError('输入文件、字段、样本量或数值不符合当前分析要求,请按本页说明检查') # 缺列时停止
work=frame.dropna(subset=list(required)).copy(); work['end_date']=pd.to_datetime(work['end_date'],errors='coerce') # 清理必需字段
for column in versions: work[column]=pd.to_datetime(work[column],errors='coerce') # 解析披露日期
keys=['ts_code','end_date']; work=work.dropna(subset=['end_date']); duplicate_mask=work.duplicated(keys,keep=False); before=int(duplicate_mask.sum()) # 统计选择前重复
if before and (not versions or work.loc[duplicate_mask,versions].isna().all(axis=1).any()): raise ValueError('输入文件、字段、样本量或数值不符合当前分析要求,请按本页说明检查') # 无版本依据时停止
work['_version']=work[versions].max(axis=1) if versions else pd.NaT # 合并双版本字段为排序键
latest_version=work.groupby(keys)['_version'].transform('max') if versions else pd.Series(pd.NaT,index=work.index) # 为每个公司报告期定位最晚披露时点
latest_rows=work.loc[~duplicate_mask | work['_version'].eq(latest_version)].copy() # 仅保留单例或并列最晚版本
tied_mask=latest_rows.duplicated(keys,keep=False); tied_latest_rows=int(tied_mask.sum()) # 统计并列最晚版本行
conflicting_latest_keys=int(latest_rows.loc[tied_mask].groupby(keys,dropna=False)[value_fields].nunique(dropna=False).gt(1).any(axis=1).sum()) if tied_latest_rows else 0 # 统计并列且数值冲突的披露键
if conflicting_latest_keys: raise ValueError(f'输入文件、字段、样本量或数值不符合当前分析要求,请按本页说明检查') # 并列最新值冲突时请先检查数据后再继续
selected=latest_rows.drop_duplicates(keys+value_fields).drop_duplicates(keys).drop(columns='_version',errors='ignore') # 仅合并数值完全一致的并列行
after=int(selected.duplicated(keys).sum()); audit={'duplicate_rows_before':before,'tied_latest_rows':tied_latest_rows,'conflicting_latest_keys':conflicting_latest_keys,'duplicate_rows_after':after,'version_fields':versions} # 返回并列与冲突检查
if after: raise ValueError('输入文件、字段、样本量或数值不符合当前分析要求,请按本页说明检查') # 选择后仍重复则停止
return selected,audit # 返回唯一版本与检查依据if is_real_data_available: # 数据可用时执行披露选择与累计转单季
bailian,version_audit = select_latest_disclosure(financials.loc[financials['ts_code'].eq('600827.SH')],['total_revenue']) # 筛选百联并保留最晚披露版本
print('披露数据版本检查:',version_audit) # 报告选择前后重复与采用字段
bailian = bailian.loc[bailian['end_date'].between('2015-01-01', '2025-12-31')].sort_values('end_date').copy() # 固定课堂样本边界与时间顺序
bailian['fiscal_year'] = bailian['end_date'].dt.year # 构造财政年键
bailian['quarter'] = bailian['end_date'].dt.quarter # 构造财政季度键
if bailian.duplicated(['fiscal_year','quarter']).any(): raise ValueError('输入文件、字段、样本量或数值不符合当前分析要求,请按本页说明检查') # 公司期间键不唯一时停止
prior_cumulative=bailian[['fiscal_year','quarter','total_revenue']].rename(columns={'total_revenue':'prior_cumulative_revenue'}).copy() # 建立前一财政季度查找表
prior_cumulative['quarter']=prior_cumulative['quarter']+1 # 将每期键前移到其紧邻后季
bailian=bailian.merge(prior_cumulative,on=['fiscal_year','quarter'],how='left',validate='one_to_one') # 按同年紧邻季度精确连接累计值
missing_predecessor=bailian['quarter'].gt(1) & bailian['prior_cumulative_revenue'].isna() # 识别Q2至Q4缺失紧邻前季的情形
if missing_predecessor.any(): raise ValueError('输入文件、字段、样本量或数值不符合当前分析要求,请按本页说明检查') # 累计转单季无法精确相减时停止
bailian['quarter_revenue'] = np.where(bailian['quarter'].eq(1),bailian['total_revenue'],bailian['total_revenue']-bailian['prior_cumulative_revenue']) # 用Q1累计值或紧邻累计差得到单季收入披露数据版本检查: {'duplicate_rows_before': 0, 'tied_latest_rows': 0, 'conflicting_latest_keys': 0, 'duplicate_rows_after': 0, 'version_fields': ['ann_date']}
if is_real_data_available: # 数据可用时构造上年同季基线与时间特征
prior_season=bailian[['fiscal_year','quarter','quarter_revenue']].rename(columns={'quarter_revenue':'seasonal_baseline'}).copy() # 建立上年同季查找表
prior_season['fiscal_year']=prior_season['fiscal_year']+1 # 将历史年键前移一年以匹配当期
bailian=bailian.merge(prior_season,on=['fiscal_year','quarter'],how='left',validate='one_to_one') # 按相同财政季度精确连接上年基线
bailian['trend_index'] = range(len(bailian)) # 构造只使用时间顺序的趋势特征if is_real_data_available: # 仅对已成功读取的真实单季数据评估
model_frame = bailian.dropna(subset=['quarter_revenue', 'seasonal_baseline']).copy() # 排除缺少同季基线的记录
model_features = pd.get_dummies(model_frame[['trend_index', 'quarter']], columns=['quarter'], drop_first=True) # 结合趋势与季节虚拟变量
training_mask = model_frame['end_date'].lt('2023-01-01') # 禁止验证期信息进入训练
validation_mask = model_frame['end_date'].between('2023-01-01', '2024-12-31') # 固定同一验证窗口
if training_mask.sum()<8 or validation_mask.sum()<2: raise ValueError('输入文件、字段、样本量或数值不符合当前分析要求,请按本页说明检查') # 拟合前检查窗口
if model_frame.loc[validation_mask,'quarter_revenue'].eq(0).any(): raise ValueError('输入文件、字段、样本量或数值不符合当前分析要求,请按本页说明检查') # 零实际值禁止进入MAPE
regression = LinearRegression().fit(model_features.loc[training_mask], model_frame.loc[training_mask, 'quarter_revenue']) # 拟合可解释候选模型
model_frame.loc[validation_mask, 'regression_forecast'] = regression.predict(model_features.loc[validation_mask]) # 生成验证期预测
training_residuals = model_frame.loc[training_mask, 'quarter_revenue'] - regression.predict(model_features.loc[training_mask]) # 估计训练残差波动
interval_half_width = 1.96 * training_residuals.std(ddof=1) # 仅在独立同分布、近似正态同方差且忽略参数不确定性时作课堂95%近似
validation = model_frame.loc[validation_mask].copy() # 隔离用于决策的验证样本
validation['prediction_lower'] = validation['regression_forecast'] - interval_half_width # 计算近似区间下界
validation['prediction_upper'] = validation['regression_forecast'] + interval_half_width # 计算近似区间上界
baseline_mape = ((validation['quarter_revenue'] - validation['seasonal_baseline']).abs() / validation['quarter_revenue'].abs()).mean() # 计算季节朴素基线MAPE
regression_mape = ((validation['quarter_revenue'] - validation['regression_forecast']).abs() / validation['quarter_revenue'].abs()).mean() # 计算候选模型MAPE
validation_r2 = r2_score(validation['quarter_revenue'], validation['regression_forecast']) # 报告拟合边界而非决策收益
if not np.isfinite([baseline_mape,regression_mape,validation_r2,interval_half_width]).all(): raise ValueError('输入文件、字段、样本量或数值不符合当前分析要求,请按本页说明检查') # 非有限依据不得进入决策
validation_display=validation[['end_date','quarter_revenue','seasonal_baseline','regression_forecast','prediction_lower','prediction_upper']].copy() # 建立紧凑课堂展示表
validation_display['end_date']=validation['end_date'].dt.year.astype(str)+'-Q'+validation['quarter'].astype(str) # 用短季度标签替代长时间戳
validation_display.columns=['季度','实际(亿元)','季节基线','趋势模型','区间下界','区间上界'] # 使用短列名避免输出换行
validation_display.iloc[:,1:]=validation_display.iloc[:,1:].div(1e8).round(1) # 统一为亿元并控制一位小数
print(validation_display.to_string(index=False)) # 每个验证季度仅占一行
print({'base_mape':round(baseline_mape,3),'model_mape':round(regression_mape,3),'R2':round(validation_r2,3),'95%半宽_亿元':round(interval_half_width/1e8,1)}) # 用简短键单行输出同一时间段指标 季度 实际(亿元) 季节基线 趋势模型 区间下界 区间上界
2023-Q1 94.9 102.9 92.7 70.5 114.8
2023-Q2 68.5 68.2 68.5 46.4 90.7
2023-Q3 72.3 80.0 66.9 44.7 89.0
2023-Q4 69.4 71.8 75.7 53.6 97.9
2024-Q1 88.3 94.9 85.3 63.1 107.4
2024-Q2 63.4 68.5 61.2 39.0 83.3
2024-Q3 64.4 72.3 59.5 37.3 81.6
2024-Q4 60.6 69.4 68.4 46.2 90.5
{'base_mape': 0.081, 'model_mape': 0.058, 'R2': 0.836, '95%半宽_亿元': 22.1}
区间边界:这是基于训练残差的教学近似,并非经过异方差、参数不确定性和结构突变校正的正式预测区间。
阈值来源:MAPE 15% 是课堂预算情景假设。决策题必须比较 10%、15%、20% 三档阈值,并说明误差成本如何改变是否采用模型。
销售预测是零售企业运营管理的核心能力:
本案例使用 沃尔玛超市销售数据(01_Walmart_Store_sales.csv):
| 字段名 | 含义 | 类型 |
|---|---|---|
| Store | 门店编号 | 分类 |
| Date | 日期 | 日期 |
| Weekly_Sales | 周销售额(目标变量) | 数值 |
| Holiday_Flag | 是否节假日 | 二值 |
| Temperature | 当地温度 | 数值 |
| 字段名 | 含义 | 类型 |
|---|---|---|
| Fuel_Price | 当地燃油价格 | 数值 |
| CPI | 消费价格指数 | 数值 |
| Unemployment | 失业率 | 数值 |
解读边界:外部变量可用于比较和建模,不能只凭相关系数解释因果。
df、target、y_vars、df1 的业务含义、数据类型或取值范围,并判断哪一个输入最可能改变结果。# 注:01_Walmart_Store_sales.csv数据文件本地没有,但平台已经内置
# ⚠️ 平台原始代码 - 请原样输入至教学平台(注释除外),平台才会判定答案正确
# 导入相关包
import pandas as pd
import numpy as np # 导入NumPy数值计算库
import seaborn as sns # 导入Seaborn可视化库
import matplotlib.pyplot as plt # 导入Matplotlib绘图库
from sklearn.preprocessing import StandardScaler # 导入Scikit-learn的StandardScaler模块
from sklearn.model_selection import train_test_split # 导入Scikit-learn的train_test_split模块
from sklearn.linear_model import LinearRegression # 导入Scikit-learn的LinearRegression模块
from sklearn.metrics import r2_score # 导入Scikit-learn的r2_score模块
# 读取数据
df = pd.read_csv('01_Walmart_Store_sales.csv', parse_dates = True, infer_datetime_format = True)
# 整理数据
df.Date=pd.to_datetime(df.Date)
df['weekday'] = df.Date.dt.weekday # 提取日期时间属性
df['month'] = df.Date.dt.month # 提取日期时间属性
df['year'] = df.Date.dt.year # 提取日期时间属性
# 删除日期、星期、年份等非特征列
df.drop(['Date','weekday','year'], axis=1, inplace=True)
target = 'Weekly_Sales' # 指定目标变量为"Weekly_Sales"
# 相关性分析和特征选择
plt.figure(figsize=(10,8))
# 绘制变量两两关系图
sns.pairplot(df, x_vars=['Holiday_Flag', 'Temperature', 'Fuel_Price', 'CPI', 'Unemployment','month'],
y_vars=['Weekly_Sales'], height=4, aspect=0.7, kind='reg') # 定义列表y_vars
plt.savefig("1.png") # 保存图形至文件
plt.show() # 显示图形
df1=df.copy(deep=True) # 深拷贝数据框用于独立分析(不影响原始数据)
df1.drop(['Store'], axis=1, inplace=True) # 删除不需要的列'Store'
corr_matrix = df1.corr() # 计算相关系数矩阵
plt.figure(figsize=(10,8)) # 创建图形画布
sns.heatmap(corr_matrix, annot=True, cmap='Reds') # 绘制热力图
plt.savefig("2.png") # 保存图形至文件
plt.show() # 显示图形
df.drop(['Fuel_Price'], axis=1, inplace=True) # 删除不需要的列'Fuel_Price'
# 分类数据处理
df = pd.get_dummies(df, columns=['Store','month'])
# 提取特征和标签
X = df.drop([target],axis=1)
y = df[target] # 提取目标变量列赋值给y
# 划分训练集和测试集
X_train, X_test, y_train, y_test = train_test_split(X, y, train_size=0.8, test_size=0.2, random_state=100)
# 数据标准化
scaler = StandardScaler()
X_train = scaler.fit_transform(X_train) # 对数据进行变换
X_test = scaler.transform(X_test) # 对数据进行变换
# 训练多元线性回归模型
model = LinearRegression()
model.fit(X_train, y_train) # 在数据上训练model模型
# 测试集预测
y_pred = model.predict(X_test)
r2 = r2_score(y_test, y_pred) # 计算R²评估模型拟合优度
# 评估模型预测效果
print('R平方值(R^2):', r2)
依据诊断:该固定答案的低边际相关删特征、随机切分、原始系数大小与单一 R² 不能支持预测部署或业务影响结论。正确的分析方法必须按时间留出,只在训练折内拟合预处理,用特征消融/滚动验证决定删留,在同一验证窗比较季节朴素基线、成本误差,并检查残差与漂移。
运行后核对:平台内只核对固定录入与输出结构;不得把该结果计入完成本章。参考答案必须写出前述正确的分析方法及明确的需要重新检查的情况。
拓展练习:把对象或期间改为一家长三角上市公司或一组 A 股资产;先预测指标方向,再说明结果能支持和不能支持的决策。
pandas、numpyseaborn、matplotlibsklearn(标准化、划分、回归、评估)parse_dates=True:自动尝试解析日期列pd.to_datetime():确保日期列格式正确保留月份的原因:销售额具有明显的季节性规律
kind='reg':添加回归拟合线,直观判断线性关系强弱annot=True:在格子中显示相关系数值| 相关系数范围 | 描述 | 合法下一步 |
|---|---|---|
| 0.7 ~ 1.0 | 强正线性关联 | 检查共线性、时间稳定性与泄漏 |
| 0.3 ~ 0.7 | 中等正线性关联 | 纳入训练期消融或滚动验证候选 |
| 0 ~ 0.3 | 弱边际线性关联 | 仍保留候选;检查非线性、交互与增量价值 |
| 负值 | 负线性关联 | 解释方向前先核对尺度、分组与时间结构 |
诊断结论:Fuel_Price 的低边际相关不能推出“无预测价值”,也不能证明因果。是否删除只能由训练期内特征消融/滚动验证决定,并在同一时间留出集上比较季节朴素基线、成本误差与残差稳定性。
独热编码 将分类变量转换为 0/1 指示变量:
Store 列(45个门店)→ 生成 45 个新列month 列(12个月份)→ 生成 12 个新列平台练习说明:下方随机
train_test_split只为受保护平台步骤兼容,不可作为时间序列上线依据。它会打乱信息边界,也没有季节朴素基线;中国主案例必须按时间留出。
方法要求:随机划分会打乱时间顺序,不能作为本章的主要预测方法。应按时间先后划分训练期和验证期,并与同一时期的季节基准预测比较,同时检查残差和数据分布变化。
random_state=100:固定随机种子,保证每次运行都能得到一致结果标准化公式:\(z = \frac{x - \mu}{\sigma}\)
线性回归的核心假设:
\[ \large y = w_1 x_1 + w_2 x_2 + \cdots + w_n x_n + b \]
即时警示:\(R^2\) 不是预测成本,也不保证优于朴素基线;脱离时间留出与误差比较,不能据此上线。
R² 决定系数 描述当前验证样本中相对均值基线的平方误差改善:
\[ \large R^2 = 1 - \frac{\sum(y_i - \hat{y}_i)^2}{\sum(y_i - \bar{y})^2} \]
| R² 值范围 | 含义 |
|---|---|
| 高于 0 | 在该样本上优于“恒预测均值”;仍需比较时间基线与成本 |
| 等于 0 | 与均值预测相当 |
| 小于 0 | 比均值预测更差;应拒绝或重新计算 |
# 注:该代码块依赖的数据来自上方平台任务代码块,因其未执行,本块也无法执行
# =============================================================================
# 题目:特征工程与特征相关性分析
# =============================================================================
# 本代码进行特征工程,包括相关性分析和特征选择。通过可视化各特征
# 与目标变量的关系,识别重要特征并剔除冗余特征,提升模型性能和
# 可解释性。这是机器学习项目中至关重要的一步。
# ==================== 定义特征列表 ====================
# 选择可能影响销售额的特征变量
features = ['Holiday_Flag', 'Temperature', 'Fuel_Price', 'CPI', 'Unemployment', 'month']
# Holiday_Flag: 假期标志,假期销售通常更高
# Temperature: 温度,可能影响购物意愿
# Fuel_Price: 燃油价格,影响消费者的出行和购买力
# CPI: 消费价格指数,衡量通胀水平
# Unemployment: 失业率,反映经济状况
# month: 月份,捕捉季节性规律
# 定义目标变量(要预测的变量)
target = 'Weekly_Sales' # 周销售额是本项目的预测目标
# ==================== 绘制散点图矩阵 ====================
# 使用Seaborn绘制散点图矩阵,展示各特征与目标变量的关系
sns.pairplot(
df, # 数据源DataFrame
x_vars=features, # x轴变量列表(所有特征)
y_vars=[target], # y轴变量列表(目标变量)
height=4, # 每个子图的高度(英寸)
aspect=0.7, # 子图的宽高比(宽度/高度)
kind='reg' # 绘制带回归线的散点图
)
plt.suptitle('各特征与销售额的关系', y=1.02) # 设置总标题,y=1.02将标题向上移动避免重叠
plt.show() # 显示散点图矩阵
# ==================== 计算相关性矩阵 ====================
# 创建数据副本,避免修改原始数据
df1 = df.copy(deep=True) # deep=True创建深拷贝,包括数据和索引
# 删除Store列(分类变量,无法计算相关系数)
df1.drop(['Store'], axis=1, inplace=True) # axis=1表示删除列,inplace=True直接在原数据上修改
# 计算所有数值变量的相关系数矩阵
corr_matrix = df1.corr() # corr()方法计算皮尔逊相关系数,返回对称矩阵
# 相关系数范围[-1, 1],绝对值越大相关性越强
# ==================== 绘制相关性热力图 ====================
plt.figure(figsize=(10, 8)) # 创建画布,尺寸为10x8英寸
# 使用Seaborn绘制热力图,可视化相关系数矩阵
sns.heatmap(corr_matrix, annot=True, cmap='Reds', center=0)
# annot=True在每个格子中显示相关系数的数值
# cmap='Reds'使用红色系配色方案(颜色越深相关性越强)
# center=0将颜色映射的中心设为0(白色表示无相关)
plt.title('特征相关性热力图') # 设置图表标题
plt.show() # 显示热力图
# ==================== 特征候选诊断 ====================
# 低边际相关不能证明Fuel_Price没有增量预测价值,因此保留至训练期内消融验证
# 是否删除须在同一时间留出窗比较季节朴素基线、成本误差与残差稳定性
# ==================== 输出解读 ====================
# 散点图矩阵展示了各特征与销售额的线性关系:
# - 如果点的分布近似直线,说明存在线性关系
# - 回归线斜率受量纲与混杂影响,只描述边际关联而非显著影响
# - 点很分散只说明当前线性边际关系弱,仍须检查交互、非线性和增量价值
#
# 热力图展示了所有特征之间的相关性:
# - 红色越深表示正相关越强
# - 蓝色越深表示负相关越强
# - 白色表示无相关
#
# 特征选择原则:
# 1. 在训练期内做特征消融,不用边际相关一票删除
# 2. 对共线性做条件诊断,并在同一时间段验证中比较泛化误差
# 3. 只有增量表现、成本与稳定性共同满足要求才保留特征依据诊断:折叠代码保留平台教学路径,但其中随机切分、固定 R² 档位、按原始系数绝对值排序及“影响销售”的措辞均不是部署依据。参考流程是时间留出 → 训练折内预处理 → 季节/朴素基线 → 消融或滚动验证 → 成本误差 → 残差与漂移检查;系数只解释为条件关联。
# 注:该代码块依赖的数据来自上方平台任务代码块,因其未执行,本块也无法执行
# =============================================================================
# 题目:构建线性回归模型预测销售额
# =============================================================================
# 本代码完成机器学习建模的完整流程:数据预处理、特征编码、数据标准化、
# 模型训练和评估。线性回归是最基础的预测模型,假设特征与目标变量之间
# 存在线性关系,适合作为基准模型。
# ==================== 分类变量编码 ====================
# 将分类变量转换为虚拟变量(独热编码)
df = pd.get_dummies(df, columns=['Store', 'month'])
# pd.get_dummies将分类变量转换为0/1指示变量
# columns参数指定要编码的列
# 例如:Store列有45个不同商店,会生成45个新列(Store_1, Store_2, ..., Store_45)
# 每行只有一个Store列为1,其余为0
# ==================== 准备特征和标签 ====================
# 提取特征矩阵X(删除目标变量列)
X = df.drop([target], axis=1) # axis=1表示删除列
# X是一个DataFrame,包含所有特征列
# 提取目标变量y(销售额)
y = df[target] # y是一个Series,包含所有销售额值
# ==================== 划分训练集和测试集 ====================
# 将数据划分为训练集(80%)和测试集(20%)
X_train, X_test, y_train, y_test = train_test_split(
X, # 特征矩阵
y, # 目标变量
train_size=0.8, # 训练集占比80%
test_size=0.2, # 测试集占比20%
random_state=100 # 随机种子,保证每次运行结果相同
)
# train_test_split随机打乱数据并按指定比例划分
# 返回四个数组:训练集特征、测试集特征、训练集标签、测试集标签
# ==================== 数据标准化 ====================
# 创建标准化器(将数据转换为均值为0、标准差为1的分布)
scaler = StandardScaler() # StandardScaler使用z-score标准化: (x - mean) / std
# 在训练集上拟合标准化器,并转换训练集数据
X_train_scaled = scaler.fit_transform(X_train) # fit计算均值和标准差,transform应用标准化
# 使用训练集的均值和标准差转换测试集数据
X_test_scaled = scaler.transform(X_test) # 注意:测试集使用训练集的统计量,不重新计算
# 这样确保模型在测试时看到的数据分布与训练时一致
# ==================== 训练线性回归模型 ====================
# 创建线性回归模型对象
model = LinearRegression() # LinearRegression实现了普通最小二乘法(OLS)
# 在训练数据上拟合模型
model.fit(X_train_scaled, y_train) # fit方法学习特征与目标变量的线性关系
# 模型学习: y = w1*x1 + w2*x2 + ... + wn*xn + b
# 其中w是权重(系数),b是截距
# ==================== 模型预测 ====================
# 使用训练好的模型预测测试集的销售额
y_pred = model.predict(X_test_scaled) # predict方法返回预测的销售额
# ==================== 模型评估 ====================
# 计算R²分数(决定系数),评估模型拟合优度
r2 = r2_score(y_test, y_pred) # R²可为负;只描述该测试样本相对均值基线的平方误差改善
# R² = 1 - (残差平方和 / 总平方和)
# 表示模型解释的方差占总方差的比例
print(f"R²值: {r2:.4f}") # 打印R²值,保留4位小数
# 将R²标为相对均值基线的样本内度量,避免解释为因果贡献
print(f"相对均值基线的R²为{r2:.4f}") # 仍须与时间基线和成本误差比较
# ==================== 输出解读 ====================
# 线性回归模型的评估指标解读:
# 1. R²值(决定系数):
# - R² = 1: 完美拟合(所有点都在回归线上)
# - 不使用通用档位;须在同一时间留出窗与季节朴素基线和成本误差比较
# - R² = 0: 模型与简单猜测(用均值预测)一样差
#
# 2. 模型系数(model.coef_):
# - 系数表示其他入模变量给定时的条件关联,不自动具有因果含义
# - 正负号描述模型中的条件方向,须结合编码、尺度和不确定性解释
# - 不同量纲下的系数绝对值不能直接比较重要性
#
# 3. 截距(model.intercept_):
# - 当所有特征为0时的销售额(基准值)
#
# 4. 残差分析:
# - 残差 = 真实值 - 预测值
# - 理想情况下,残差应服从正态分布,均值为0
# - 如果残差有模式,说明模型有改进空间
#
# 5. 实际应用:
# - 超市可以根据预测结果提前备货
# - 识别需要进一步消融、稳健性和业务验证的候选因素
# - 优化营销策略和人员安排stock/financial_statement.h5(key: financial_data);百联股份 600827.SH;标准化字段 ts_code/end_date/total_revenue,可选版本字段 ann_date/f_ann_datelst-ch22-local-bailian-baseline 完成版本去重、累计收入转单季值、固定时间切分和上年同季基线;lst-ch22-local-bailian-evaluation 在同一验证窗生成趋势回归、两个 MAPE 与近似 95% 区间。以下判断条件代码完成三档决策,三部分共同逐项覆盖 20/30/25/25 分检查。from pathlib import Path # 导入路径工具以定位规定财报快照
import numpy as np # 导入数值工具以做有限值检查要求
import pandas as pd # 导入表格工具以处理季度财报
from sklearn.linear_model import LinearRegression # 导入趋势回归候选模型
from sklearn.metrics import r2_score # 导入R方以描述验证期拟合边界
financial_path=Path('/home/ubuntu/r2_data_mount/data/stock/financial_statement.h5') # 绑定百联股份财报快照
if not financial_path.exists(): raise FileNotFoundError('未找到课程数据文件,请从课程数据下载入口获取并核对文件位置') # 缺少规定资产时终止
financials=pd.read_hdf(financial_path,key='financial_data',where="order_book_id == '600827.XSHG'",columns=['order_book_id','quarter','info_date','operating_revenue']).rename(columns={'order_book_id':'ts_code','quarter':'end_date','info_date':'ann_date','operating_revenue':'total_revenue'}) # 按公司读取必需披露字段
financials['ts_code']=financials['ts_code'].str.replace('.XSHG','.SH',regex=False) # 将证券代码后缀统一为课程口径
financials['end_date']=pd.PeriodIndex(financials['end_date'].str.upper(),freq='Q').end_time.normalize() # 将季度键转为报告期末日
financials['ann_date']=pd.to_datetime(financials['ann_date'],errors='coerce') # 解析披露日侜为版本排序键
if financials[['ts_code','end_date','ann_date']].isna().any().any(): raise ValueError('输入文件、字段、样本量或数值不符合当前分析要求,请按本页说明检查') # 版本键缺失时请先检查数据后再继续
bailian=financials.sort_values(['ts_code','end_date','ann_date']).drop_duplicates(['ts_code','end_date'],keep='last').copy() # 仅保留每个报告期最晚披露版本
bailian=bailian.loc[bailian['end_date'].between('2015-01-01','2025-12-31')].sort_values('end_date').copy() # 固定十一年样本边界
bailian['fiscal_year']=bailian['end_date'].dt.year # 构造财政年键
bailian['quarter']=bailian['end_date'].dt.quarter # 构造财政季度键
prior_cumulative=bailian[['fiscal_year','quarter','total_revenue']].rename(columns={'total_revenue':'prior_cumulative_revenue'}).copy() # 建立前一季累计收入查找表
prior_cumulative['quarter']=prior_cumulative['quarter']+1 # 把查找键前移到后一季
bailian=bailian.merge(prior_cumulative,on=['fiscal_year','quarter'],how='left',validate='one_to_one') # 按同年紧邻季度连接累计值
if (bailian['quarter'].gt(1)&bailian['prior_cumulative_revenue'].isna()).any(): raise ValueError('输入文件、字段、样本量或数值不符合当前分析要求,请按本页说明检查') # 缺前季时禁止差分
bailian['quarter_revenue']=np.where(bailian['quarter'].eq(1),bailian['total_revenue'],bailian['total_revenue']-bailian['prior_cumulative_revenue']) # 把累计值转为单季收入prior_season=bailian[['fiscal_year','quarter','quarter_revenue']].rename(columns={'quarter_revenue':'seasonal_baseline'}).copy() # 建立上年同季基线查找表
prior_season['fiscal_year']=prior_season['fiscal_year']+1 # 将历史财政年键前移一年
bailian=bailian.merge(prior_season,on=['fiscal_year','quarter'],how='left',validate='one_to_one') # 按同季连接上年基线
bailian['trend_index']=range(len(bailian)) # 构造仅依赖时间顺序的趋势特征
model_frame=bailian.dropna(subset=['quarter_revenue','seasonal_baseline']).copy() # 仅保留可同一时间段比较的季度
model_features=pd.get_dummies(model_frame[['trend_index','quarter']],columns=['quarter'],drop_first=True) # 构造趋势与季节虚拟特征
training_mask=model_frame['end_date'].lt('2023-01-01') # 固定2023年前为训练窗
validation_mask=model_frame['end_date'].between('2023-01-01','2024-12-31') # 固定2023—2024为独立验证窗
if training_mask.sum()<8 or validation_mask.sum()<2: raise ValueError('输入文件、字段、样本量或数值不符合当前分析要求,请按本页说明检查') # 训练或验证季度不足时终止
if model_frame.loc[validation_mask,'quarter_revenue'].eq(0).any(): raise ValueError('输入文件、字段、样本量或数值不符合当前分析要求,请按本页说明检查') # 验证实际值为零时禁用MAPE
regression=LinearRegression().fit(model_features.loc[training_mask],model_frame.loc[training_mask,'quarter_revenue']) # 仅用训练窗拟合候选模型
model_frame.loc[validation_mask,'regression_forecast']=regression.predict(model_features.loc[validation_mask]) # 生成验证窗趋势预测
training_residuals=model_frame.loc[training_mask,'quarter_revenue']-regression.predict(model_features.loc[training_mask]) # 仅从训练残差估计区间宽度
interval_half_width=1.96*training_residuals.std(ddof=1) # 在近似正态同方差且忽略参数不确定性时计算课堂95%近似半宽
validation=model_frame.loc[validation_mask].copy() # 隔离用于模型决策的验证表
validation['prediction_lower']=validation['regression_forecast']-interval_half_width # 计算近似区间下界
validation['prediction_upper']=validation['regression_forecast']+interval_half_width # 计算近似区间上界
baseline_mape=((validation['quarter_revenue']-validation['seasonal_baseline']).abs()/validation['quarter_revenue'].abs()).mean() # 计算上年同季基线MAPE
regression_mape=((validation['quarter_revenue']-validation['regression_forecast']).abs()/validation['quarter_revenue'].abs()).mean() # 计算趋势模型MAPE
validation_r2=r2_score(validation['quarter_revenue'],validation['regression_forecast']) # 计算验证窗R方作为描述边界
if not np.isfinite([baseline_mape,regression_mape,validation_r2,interval_half_width]).all(): raise ValueError('输入文件、字段、样本量或数值不符合当前分析要求,请按本页说明检查') # 非有限依据不得进入决策from pathlib import Path # 导入路径工具检查规定资产
financial_path=Path('/home/ubuntu/r2_data_mount/data/stock/financial_statement.h5') # 绑定百联股份财报快照
if not financial_path.exists(): raise FileNotFoundError('未找到课程数据文件,请从课程数据下载入口获取并核对文件位置') # 缺失时如实停止
cost_thresholds=[0.10,0.15,0.20] # 定义课堂成本敏感性情景
decision_owner='零售预测负责人(课堂角色,待业务确认)' # 指定课堂责任角色
recheck_date='下一季度财报公布后第5个工作日' # 事先设定再次检查日期规则
decision_table=[] # 初始化逐情景决策表
for cost_threshold in cost_thresholds: # 逐档比较同一时间段模型误差
if regression_mape<baseline_mape and regression_mape<=cost_threshold: # 判断候选模型是否同时优于基线并过最低要求
decision_status='ADOPT_REGRESSION' # 采用回归并进入监控
elif baseline_mape<=cost_threshold: # 判断朴素基线是否满足预算
decision_status='KEEP_BASELINE' # 保留较稳健的季节基线
else: # 两种方法都不能满足成本最低要求
decision_status='暂不采用' # 暂停上线并调查数据与结构变化
action={'ADOPT_REGRESSION':'小范围试用并继续观察','KEEP_BASELINE':'保留基准方法并核对','暂不采用':'暂不采用并检查原因'}[decision_status] # 记录建议
recheck_when='实际MAPE连续两期超过该课堂阈值或输入字段改变' # 明确升级与需要重新检查的情况
decision_table.append({'threshold':cost_threshold,'threshold_status':'课堂情景,待业务规定/预算批准','decision':decision_status,'owner':decision_owner,'action':action,'recheck_when':recheck_when,'recheck_date':recheck_date}) # 保存完整决策依据
print(validation,decision_table) # 输出验证表与三档决策case 资产;随机切分;模型比较使用不同一时间段口[商业大数据分析与应用]